/*-------------------------------------------------------------------------
 *
 * sinval.c
 *	  POSTGRES 共享缓存失效通信代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/storage/ipc/sinval.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/xact.h"
#include "commands/async.h"
#include "miscadmin.h"
#include "nodes/memnodes.h"
#include "storage/ipc.h"
#include "storage/proc.h"
#include "storage/sinvaladt.h"
#include "utils/inval.h"


uint64		SharedInvalidMessageCounter;


/*
 * 因为空闲的后台将不会读取 sinval 事件，我们需要一种方法给空闲的后台一个快速的踢，使其赶上进度，以免 sinval 队列溢出并迫使它经历缓存重置的过程。这是通过向任何落后太多的后台发送 PROCSIG_CATCHUP_INTERRUPT 来实现的。
 *
 * 信号处理程序将设置一个中断挂起标志，并将设置进程的闩锁。每当开始从客户端读取时，或者在这样做时被中断，ProcessClientReadInterrupt() 将调用 ProcessCatchupEvent()。
 */
volatile sig_atomic_t catchupInterruptPending = false;


/*
 * SendSharedInvalidMessages
 * 将共享缓存失效消息添加到全局 SI 消息队列。
 */
void SendSharedInvalidMessages(const SharedInvalidationMessage *fc_msgs, int fc_n)
{
	SIInsertDataEntries(fc_msgs, fc_n);
}

/*
 * ReceiveSharedInvalidMessages
 * 处理等待此后台的共享缓存失效消息
 *
 * 我们保证处理在例程进入之前已经排队的所有消息。当然，可能在我们最后一次调用 SIGetDataEntries 之后会有更多的消息被排队。
 *
 * 注意：由于在处理 invalFunction 或 resetFunction 的过程中，这个例程可能会被递归调用。 此外，这种递归调用必须保证在退出之前处理所有未完成的 inval 消息。这就是使用静态分配的缓冲区数组和计数器的奇怪选择的原因；这样递归调用可以处理已经从 sinvaladt.c 中获取的消息。
 */
void ReceiveSharedInvalidMessages(void (*fc_invalFunction) (SharedInvalidationMessage *msg),
							 void (*fc_resetFunction) (void))
{
#define MAXINVALMSGS 32
	static SharedInvalidationMessage fc_messages[MAXINVALMSGS];

	/*
	 * 我们在这里使用 volatile 以防止编译器未意识到递归是一种可能性而产生错误...
	 */
	static volatile int fc_nextmsg = 0;
	static volatile int fc_nummsgs = 0;

	/* 处理任何来自外部递归的仍在挂起的消息 */
	while (fc_nextmsg < fc_nummsgs)
	{
		SharedInvalidationMessage fc_msg = fc_messages[fc_nextmsg++];

		SharedInvalidMessageCounter++;
		fc_invalFunction(&fc_msg);
	}

	do
	{
		int			fc_getResult;

		fc_nextmsg = fc_nummsgs = 0;

		/* 尝试获取更多消息 */
		fc_getResult = SIGetDataEntries(fc_messages, MAXINVALMSGS);

		if (fc_getResult < 0)
		{
			/* 得到一个重置消息 */
			elog(DEBUG4, "cache state reset");
			SharedInvalidMessageCounter++;
			fc_resetFunction();
			break;				/* 没有更多的工作要做 */
		}

		/* 处理它们，注意递归调用可能会消耗一些消息 */
		fc_nextmsg = 0;
		fc_nummsgs = fc_getResult;

		while (fc_nextmsg < fc_nummsgs)
		{
			SharedInvalidationMessage fc_msg = fc_messages[fc_nextmsg++];

			SharedInvalidMessageCounter++;
			fc_invalFunction(&fc_msg);
		}

		/*
		 * 只有在最后一次 SIGetDataEntries 调用（可能
		 * 在递归调用中）返回完整缓冲区时，我们才需要循环。
		 */
	} while (fc_nummsgs == MAXINVALMSGS);

	/*
	 * 我们现在已经赶上进度。如果我们收到一个赶上信号，重置该标志，并调用 SICleanupQueue()。这并不是因为我们现在需要刷新死消息，而是因为我们想将赶上信号传递给下一个最慢的后台。通过这种方式“链式传递”赶上信号可以避免在应该仅为后台维护活动的情况下在系统负载中产生峰值。
	 */
	if (catchupInterruptPending)
	{
		catchupInterruptPending = false;
		elog(DEBUG4, "sinval catchup complete, cleaning queue");
		SICleanupQueue(false, 0);
	}
}


/*
 * HandleCatchupInterrupt
 *
 * 当接收到 PROCSIG_CATCHUP_INTERRUPT 时调用。
 *
 * 我们过去是在空闲时直接调用 ProcessCatchupEvent。如今，我们只设置一个标志以便稍后执行，并通过设置进程的闩锁来通知该进程这一信息。
 */
void HandleCatchupInterrupt(void)
{
	/*
	 * 注意：这是由信号处理程序调用的。您必须非常小心在这里做什么。
	 */

	catchupInterruptPending = true;

	/* make sure the event is processed in due course */
	SetLatch(MyLatch);
}

/*
 * ProcessCatchupInterrupt
 *
 * 执行在信号处理程序外部运行的赶上中断处理，这允许它实际处理待处理的无效消息。
 */
void ProcessCatchupInterrupt(void)
{
	while (catchupInterruptPending)
	{
		/*
		 * 我们这里需要做的是使 ReceiveSharedInvalidMessages() 运行，这将完成必要的工作并重置 catchupInterruptPending 标志。如果我们处于事务中，我们可以简单调用 AcceptInvalidationMessages() 来完成此操作。如果我们不在事务中，我们开始并立即结束一个事务；对 AcceptInvalidationMessages() 的调用发生在事务开始内部。确保在执行时保留调用者的内存上下文。
		 *
		 * 确实很想直接调用 AcceptInvalidationMessages()，而没有其他的事务启动/停止开销，我认为在正常情况下这样做实际上是可行的；但我不确定如果我们在中途遇到错误事情是否会处理得很好。
		 */
		if (IsTransactionOrTransactionBlock())
		{
			elog(DEBUG4, "ProcessCatchupEvent inside transaction");
			AcceptInvalidationMessages();
		}
		else
		{
			MemoryContext fc_oldcontext = CurrentMemoryContext;

			elog(DEBUG4, "ProcessCatchupEvent outside transaction");
			StartTransactionCommand();
			CommitTransactionCommand();
			/* 调用者的上下文最好不要是事务本地的 */
			Assert(MemoryContextIsValid(fc_oldcontext));
			MemoryContextSwitchTo(fc_oldcontext);
		}
	}
}
